/// Helper utilities for testing.
library;

import 'dart:async';

import 'package:async/async.dart';

class TestFailure implements Exception {
  final String? message;

  TestFailure(this.message);

  @override
  String toString() => message.toString();
}

/// A zero-millisecond timer should wait until after all microtasks.
Future flushMicrotasks() => Future.delayed(Duration.zero);

typedef OptionalArgAction = void Function([dynamic a, dynamic b]);

Future pumpEventQueue({int times = 20}) {
  if (times == 0) return Future.value();
  return Future(() => pumpEventQueue(times: times - 1));
}

OptionalArgAction unreachable(String name) =>
        ([a, b]) => fail('Unreachable: $name');

Never fail(String message) => throw TestFailure(message);

/// A badly behaved stream which throws if it's ever listened to.
///
/// Can be used to test cases where a stream should not be used.
class UnusableStream<T> extends Stream<T> {
  @override
  StreamSubscription<T> listen(void Function(T event)? onData,
      {Function? onError, void Function()? onDone, bool? cancelOnError}) {
    throw UnimplementedError('Gotcha!');
  }
}

/// A dummy [StreamSink] for testing the routing of the [done] and [close]
/// futures.
///
/// The [completer] field allows the user to control the future returned by
/// [done] and [close].
class CompleterStreamSink<T> implements StreamSink<T> {
  final completer = Completer();

  @override
  Future get done => completer.future;

  @override
  void add(T event) {}
  @override
  void addError(Object error, [StackTrace? stackTrace]) {}
  @override
  Future addStream(Stream<T> stream) async {}
  @override
  Future close() => completer.future;
}

/// A [StreamSink] that collects all events added to it as results.
///
/// This is used for testing code that interacts with sinks.
class TestSink<T> implements StreamSink<T> {
  /// The results corresponding to events that have been added to the sink.
  final results = <Result<T>>[];

  /// Whether [close] has been called.
  bool get isClosed => _isClosed;
  var _isClosed = false;

  @override
  Future get done => _doneCompleter.future;
  final _doneCompleter = Completer();

  final void Function() _onDone;

  /// Creates a new sink.
  ///
  /// If [onDone] is passed, it's called when the user calls [close]. Its result
  /// is piped to the [done] future.
  TestSink({void Function()? onDone}) : _onDone = onDone ?? (() {});

  @override
  void add(T event) {
    results.add(Result<T>.value(event));
  }

  @override
  void addError(Object error, [StackTrace? stackTrace]) {
    results.add(Result<T>.error(error, stackTrace));
  }

  @override
  Future addStream(Stream<T> stream) {
    var completer = Completer.sync();
    stream.listen(add, onError: addError, onDone: completer.complete);
    return completer.future;
  }

  @override
  Future close() {
    _isClosed = true;
    _doneCompleter.complete(Future.microtask(_onDone));
    return done;
  }
}
